import { join } from 'path'
import fetch from 'cross-fetch'
import { StringUtils } from 'turbocommons-ts'
import type { JSONSchema4 } from 'json-schema'
import { ESLint } from 'eslint'
import chalk from 'chalk'
import { createLogger } from '../logger'
import type { Api, GroupItem, ProjectInfoResponse, Recordable, RequestMethod, Method } from '../types'
import { emptyDir, obj2SearchString, writeFile, writeToFile } from '../utils'

const logger = createLogger('info', { prefix: '[Gen api]' })

interface ProcessedApi {
  method: {
    name: string
    path: string
    method: RequestMethod
    query: string | null
    param: string | null
    response: string | null
  }
  fileName: string
}

const cwd = process.cwd()

function getTSMethod({ name, query, method, path, param, response }: Method) {
  const argStr = query || param ? `params: { ${query ? `query: ${query}` : ''} ${query && param ? ',' : ''} ${param ? `param: ${param}` : ''} },` : ''
  return `${name}(${argStr} options?: RequestOptions) { \n
    return Request.${method.toLowerCase()}<${response || 'any'}>('${path}', { ${query || param ? '...params, ' : ''} ...options }) \n
  }\n`
}

function getJsMethod({ name, query, method, path, param }: Method) {
  const argStr = query || param ? `{ ${query ? 'query' : ''} ${query && param ? ',' : ''} ${param ? 'param' : ''} },` : ''
  return `${name}(${argStr} options={}) { \n
    return Request.${method.toLowerCase()}('${path}', { ${argStr.replace('{', '').replace('}', '')} ...options }) \n
  }\n`
}

function getTSClass(className: string, fileName: string, methods: Method[]) {
  const models = Array.from(new Set(methods.reduce((acc, current) => {
    acc.push(current.query)
    acc.push(current.response)
    acc.push(current.param)
    return acc
  }, [] as any[]))).filter(item => item)

  return `import { Request } from '@/utils/request/index' \n 
    import { RequestOptions } from '@/utils/request/types' \n
    ${models.length > 0
    ? `import {
      ${models.join(',\n')}
    } from '../models${fileName}'; \n`
    : ''}
    class ${className}Service { \n
${
  methods.map(getTSMethod).join('\n')
}
    } \n
    \n
    export const {
    ${
  methods.map(item => item.name).join(',\n')
}
    } = new ${className}Service()
  `
}

function getJSClass(className: string, fileName: string, methods: Method[]) {
  return `import { Request } from '@/utils/request' \n 
    
    \n
    class ${className}Service { \n
${
  methods.map(getJsMethod).join('\n')
}
    } \n
    \n
    export const {
    ${
  methods.map(item => item.name).join(',\n')
}
    } = new ${className}Service()
  `
}

export class Generator {
  // YAPI_URL
  fromUrl = ''
  // YAPI_TOKEN
  token = ''
  // 请求前缀
  prefix = ''
  // 请求yapi参数
  strParams = ''
  // service生成目录
  servicePath = ''
  // model生成目录
  modelPath = ''
  // 生成类型 js | ts
  type = ''

  constructor(fromUrl: string, token: string, prefix: string, type = 'js') {
    this.fromUrl = fromUrl
    this.token = token
    this.prefix = prefix

    this.servicePath = join(cwd, '/src/api/services')
    this.modelPath = join(cwd, '/src/api/models')

    this.strParams = obj2SearchString({
      token: this.token,
      type: 'json',
      isWiki: false,
      status: 'all'
    })

    this.type = type
  }

  _getProjectInfo(): Promise<ProjectInfoResponse> {
    logger.info('拉取项目信息中...')
    return fetch(`${this.fromUrl}/api/project/get?${this.strParams}`)
      .then(res => res.json() as Promise<ProjectInfoResponse>)
  }

  _getApiInfo(): Promise<GroupItem[]> {
    logger.info('拉取接口信息中...')
    return fetch(`${this.fromUrl}/api/plugin/export?${this.strParams}`)
      .then(res => res.json() as Promise<GroupItem[]>)
  }

  _emptyTargetDir() {
    logger.info('清空原有目录中...')
    emptyDir(this.servicePath)
    emptyDir(this.modelPath)
  }

  _getFileNameByPath(filePath: string) {
    if (filePath) {
      const fileName = StringUtils.formatCase(
        filePath.split('/')[0],
        StringUtils.FORMAT_LOWER_CAMEL_CASE
      )

      if (!fileName) {
        return ''
      }

      return `/${fileName}.${this.type}`
    }
    return ''
  }

  async _processApi(api: Api, basePath: string): Promise<ProcessedApi> {
    const replacedPrefix = new RegExp(`^${this.prefix.replace(basePath, '')}\/`, 'g')
    // 清空path中prefix部分
    const replacedPath = api.path.replace(replacedPrefix, '')
    const fileName = this._getFileNameByPath(replacedPath)
    if (!fileName) {
      return { method: {} as any, fileName }
    }
    const camelCasedName = StringUtils.formatCase(replacedPath, StringUtils.FORMAT_UPPER_CAMEL_CASE)

    let hasQuery = false
    let hasParam = false
    let hasResponse = false

    // 接口参数
    if (Reflect.has(api, 'req_query') || Reflect.has(api, 'req_body_other')) {
      // 处理Query
      if (api.req_query) {
        const schema = { type: 'object', properties: {}, $schema: 'http://json-schema.org/draft-04/schema#' } as JSONSchema4
        api.req_query.forEach((item) => {
          schema.properties![item.name] = { type: 'string' }
        })
        if (api.req_query.length > 0) {
          if (this.type === 'ts') {
            await writeToFile(schema, join(this.modelPath, fileName), `${camelCasedName}Query`)
          }
          hasQuery = true
        }
      }

      // 处理Body
      if (api.req_body_other && api.req_body_is_json_schema) {
        if (this.type === 'ts') {
          await writeToFile(JSON.parse(api.req_body_other), join(this.modelPath, fileName), `${camelCasedName}Param`)
        }
        hasParam = true
      }

      // TODO: 改天有空看看form的返回值，再处理的导出
      if (Array.isArray(api.req_body_form) && api.req_body_form.length > 0) {
        hasParam = true
      }
    }

    // 接口返回值
    if (Reflect.has(api, 'res_body') && api.res_body && api.res_body_is_json_schema) {
      const replaced = api.res_body.replace(',\"default\":\"Collections.emptyList()\"', '')
      if (this.type === 'ts') {
        await writeToFile(JSON.parse(replaced), join(this.modelPath, fileName), `${camelCasedName}Response`)
      }
      hasResponse = true
    }

    const method = {
      name: StringUtils.formatCase(`${camelCasedName}Api`, StringUtils.FORMAT_LOWER_CAMEL_CASE),
      path: `/${replacedPath}`,
      method: api.method,
      query: hasQuery ? `${camelCasedName}Query` : null,
      param: hasParam ? `${camelCasedName}Param` : null,
      response: hasResponse ? `${camelCasedName}Response` : null
    }
    return { method, fileName }
  }

  async _getAPiInfoFromJson(data: GroupItem[], basePath: string) {
    const result: Recordable = {}

    for (let i = 0; i < data.length; i += 1) {
      const groupItem = data[i]
      for (let j = 0; j < groupItem.list.length; j += 1) {
        const api = groupItem.list[j]
        const { method, fileName } = await this._processApi(api, basePath)
        if (!fileName) {
          continue
        }
        if (!result[fileName]) {
          result[fileName] = []
        }
        result[fileName].push(method)
      }
    }

    Object.keys(result).forEach((fileName) => {
      result[fileName] = result[fileName].sort(function(api1: Method, api2: Method) {
        return api1.path.localeCompare(api2.path)
      })
    })

    return result
  }

  _writeToFile(apiInfo: any) {
    return new Promise((resolve) => {
      Object.keys(apiInfo).forEach((fileName) => {
        const replacedFileName = fileName.replace(`.${this.type}`, '')

        let classContent = ' '
        if (this.type === 'ts') {
          classContent = getTSClass(
            StringUtils.formatCase(replacedFileName, StringUtils.FORMAT_UPPER_CAMEL_CASE), replacedFileName, apiInfo[fileName]
          )
        } else {
          classContent = getJSClass(
            StringUtils.formatCase(replacedFileName, StringUtils.FORMAT_UPPER_CAMEL_CASE), replacedFileName, apiInfo[fileName]
          )
        }
        writeFile(join(this.servicePath, fileName), classContent)
      })
      resolve('')
    })
  }

  async _runEslint() {
    logger.info(chalk.green('生成完成，开始执行 eslint'))
    const eslint = new ESLint({ fix: true })

    const results = await eslint.lintFiles(join(cwd, `src/api/**/*.${this.type}`))
    // 3. 用固定代码修改文件。
    await ESLint.outputFixes(results)
    logger.info(chalk.green('eslint 执行完成'))
  }

  generate() {
    let basePath = ''
    this._getProjectInfo()
      .then((projectInfo) => {
        basePath = projectInfo.data.basepath
      })
      .then(() => this._getApiInfo())
      .then((data) => {
        this._emptyTargetDir()
        logger.info('开始准备生成')
        return this._getAPiInfoFromJson(data, basePath)
      })
      .then(data => this._writeToFile(data))
      .then(() => this._runEslint())
  }
}
